/*
    This file is part of Emu.

    Emu is free software: you can redistribute it and/or modify
    it under the terms of the GNU Lesser General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    Emu is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Lesser General Public License for more details.

    You should have received a copy of the GNU Lesser General Public License
    along with Emu.  If not, see <http://www.gnu.org/licenses/>. */
#include <QCoreApplication>
#include <QFile>
#include <QKeyEvent>
#include <QMessageBox>
#include <QScrollBar>
#include <QTextBlock>
#include <QVBoxLayout>

#include "etelnetdefines.h"
#include "ecolordefs.h"
#include "eansidefines.h"
#include "esession.h"

/* This starts a telnet command. */
#define IAC (char)255

ESession::ESession(EProfile *profile)
{
    _profile = profile;

    /* The splitter handles the construction of all the important widgets, and
       provides functions to access them. */
    _splitter = new ESplitter;
    _recvWindow = _splitter->recvWindow();
    _recvCursor = QTextCursor(_recvWindow->textCursor());
    _recvFormat = _recvCursor.charFormat();
    _recvFormat.setProperty(FG_PROP, C_FOREGROUND);
    _recvFormat.setProperty(BG_PROP, C_BACKGROUND);

    _searchWindow = _splitter->searchWindow();
    _searchBar = _splitter->searchBar();

    _sendWindow = _splitter->sendWindow();

    _textIndex = 0;
    _socket = new QTcpSocket;
    _socketState = SocketNeverConnected;
    _telnetState = TelnetText;
    _telnetEscapeOnSE = false;
    _ansiAdjust = 0;
    _ansiAttributes = 0;
    _ansiBufferIndex = 0;
    _ansiCurrentCode = 0;
    _ansiState = AnsiText;

    /* This tracks if this session is the current one. */
    _current = false;

    /* Set the initial foreground and background colors according to the profile
       that was given. */
    updateDisplay();

    /* For reading socket data. */
    connect(_socket, SIGNAL(readyRead()), this, SLOT(readyRead()));

    _inputBuffer = (char *)malloc(4096 * sizeof(char));

    /* Focus in trapping, prev+next history keys, macros, send text on
       enter/return. */
    _sendWindow->installEventFilter(this);
    /* Focus in trapping. */
    _recvWindow->installEventFilter(this);
    /* Focus in trapping, filtering Shift+Enter, close on escape. */
    _searchBar->searchWindow()->installEventFilter(this);
}

ESession::~ESession()
{
    if (_socket->isValid())
        /* Qt docs say the socket closes when deleted. However, one version of
           Qt didn't do this, so sockets were left hanging open. Ensure that the
           socket is closed. */
        _socket->close();

    /* Delete the socket too, for good measure. */
    delete _socket;

    free(_inputBuffer);

    delete _profile;
}

void ESession::connected()
{
    writeMessage(tr("Connected to host at %1 on port %2.\n")
            .arg(_profile->hostAddress())
            .arg(_profile->hostPort()));

    _socketState = SocketConnected;
}

void ESession::error(QAbstractSocket::SocketError)
{
    writeMessage(tr("Failed to connect to host at %1 on port %2.\n")
            .arg(_profile->hostAddress())
            .arg(_profile->hostPort()));
}

void ESession::disconnected()
{
    writeMessage(tr("Disconnected from host.\n"));

    _socketState = SocketDisconnected;
}

/* Match the data in _catchBuffer to any triggers.
   Returns true if a matching or filtering trigger was found.
   bytesRead is number of bytes in _catchBuffer, including \0. */
bool ESession::processTriggers()
{
    /* There are three types of triggers : Those that highlight text, those that
       omit the text, and those that allow the text to pass untouched. */
    enum MatchedTriggerType {
        TypeNone,
        TypeHighlight,
        TypeOmit
    } trigType = TypeNone;

    bool haveAnsi;
    char patchChar;
    int i, patchAt, options;
    ETrigger matchedTrigger;
    QList <ETrigger> triggers = _profile->triggers();
    QColor highlightColor;

    haveAnsi = (_ansiBufferIndex != _textIndex);

    /* Make sure that \n (or \r\n) is not included in trigger matching.
       There's only a guarantee of 1 other character besides \0, so the
       first test needs a buf size check. */
    if (_textIndex >= 2 && _textBuffer[_textIndex - 2] == '\r') {
        patchAt = _textIndex - 2;
        patchChar = '\r';
    }
    else if (_textBuffer[_textIndex - 1] == '\n' ||
             _textBuffer[_textIndex - 1] == '\r') {
        patchAt = _textIndex - 1;
        patchChar = _textBuffer[_textIndex - 1];
    }
    else {
        patchAt = _textIndex;
        patchChar = '\0';
    }

    for (i = 0;i < triggers.count();i++) {
        ETrigger trigger = triggers.at(i);
        if (trigger.exactMatch(_textBuffer)) {
            options = trigger.options();
            /* Make sure the trigger has a reply, instead of assuming it does
               and possibly sending blank lines. */
            if (options & TRIG_HASREPLY) {
                /* Capturing triggers (always regexps) require QString. */
                if (options & TRIG_CAPTURING) {
                    QString copy = QString(_textBuffer);
                    copy.replace((QRegExp)trigger.pattern(), trigger.output());
                    _socket->write(copy.toUtf8() + "\r\n");
                }
                else
                    _socket->write(trigger.output().toUtf8() + "\r\n");
            }
            /* Make a note of if this trigger deletes or highlights the text.
               It doesn't have to do either.
               In the event of a highlighting trigger and a filtering trigger
               matching the same text, this will pick whichever came first.
               The assumption is that the user will put important triggers near
               the top. */
            if (trigType == TypeNone) {
                if (options & TRIG_OMIT)
                    trigType = TypeOmit;
                else if (options & TRIG_HIGHLIGHT) {
                    trigType = TypeHighlight;
                    highlightColor = trigger.highlightColor();
                }
            }
            /// CHECK THIS.
            if (!(options & TRIG_KEEPGOING))
                break;
        }
    }

    /* This either does nothing or fixes the line back to how it should be. */
    _textBuffer[patchAt] = patchChar;

    if (trigType == TypeOmit) {
        /* Keep up with ANSI codes, but don't show the text. */
        if (haveAnsi || _ansiState != AnsiText)
            processAnsi(false);
    }
    else if (trigType == TypeHighlight) {
        /* Show the text in the appropriate color. */
        QColor storeColor;
        int storeProp;

        storeColor = _recvFormat.foreground().color();
        storeProp = _recvFormat.property(FG_PROP).toInt();

        _recvFormat.setForeground(highlightColor);
        _recvFormat.setProperty(FG_PROP, C_USER_HIGHLIGHT);
        _recvCursor.insertText(_textBuffer, _recvFormat);

        _recvFormat.setForeground(storeColor);
        _recvFormat.setProperty(FG_PROP, storeProp);
        _textSent = true;

        /* Keep up with any ANSI codes, but don't write anything. */
        if (haveAnsi || _ansiState != AnsiText)
            processAnsi(false);
    }

    return (trigType == TypeNone);
}

bool ESession::eventFilter(QObject *obj, QEvent *event)
/* This is used to handle macros, history, focus in trapping, and also does a
   few nifty features. */
{
    if (obj == _sendWindow) {
        if (event->type() == QEvent::KeyPress) {
            QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event);

            int key = keyEvent->key();
            int mods = keyEvent->modifiers();
            int press = key + mods;

            /* This is where the history back/forward is handled. */
            if (mods == Qt::AltModifier) {
                if (key == Qt::Key_Up) {
                    _sendWindow->historyUp();
                    return true;
                }
                else if (key == Qt::Key_Down) {
                    _sendWindow->historyDown();
                    return true;
                }
            }

            /* Does this keypress match a macro? */
            int to;
            QString macroStr = _profile->macroText(press, &to);
            if (!macroStr.isEmpty()) {
                if (to == false)
                    /* It goes to the server. */
                    _socket->write(macroStr.toUtf8() + "\r\n");
                else
                    /* The text goes to the input window. */
                    _sendWindow->textCursor().insertText(macroStr);
            }

            /* Should this be sent to the server? */
            if (key == Qt::Key_Enter || key == Qt::Key_Return) {
                QString text = _sendWindow->toPlainText();

                text = _profile->aliasReplace(text);

                /* If empty, do nothing. The second part of this is a feature
                   that isn't mentioned except here. The idea is that a user can
                   put ~ at the beginning of text and not have to worry about
                   an accidental Enter/Return sending the text early.
                   Type what you want, go back, delete the ~, and send. */
                if (text.length() == 0 || text.at(0) == QChar('~'))
                    return true;

                /* Send the text, then record it in history. */
                _socket->write(text.toUtf8() + "\r\n");
                _sendWindow->historyAdd();
                _sendWindow->clear();

                return true;
            }
            /* If the next session menu option is disabled, Qt will insert a
               tab if it's pressed. Don't allow this, because servers will
               sometimes remove the tab anyway. */
            else if (key == Qt::Key_Tab && mods != Qt::NoModifier)
                return true;
            else if ((key == Qt::Key_PageUp || key == Qt::Key_PageDown) &&
                      mods == Qt::NoModifier) {
                /* Send these keys to the top window, because the bottom window
                   is not likely to be using them. */
                QCoreApplication::sendEvent(_recvWindow, event);
                return true;
            }
            else if ((key == Qt::Key_End || key == Qt::Key_Home) &&
                     mods == Qt::ControlModifier) {
                /* Send Ctrl+Home and Ctrl+End to the top window as Home and
                   End. This allows controlling the top window from the bottom
                   window without disabling Home+End in the bottom window. */
                QKeyEvent *newEvent;
                newEvent = new QKeyEvent(QEvent::KeyPress, key, 0);
                /* This does the same as sendEvent, and also deletes newEvent
                   when it is done. */
                QCoreApplication::postEvent(_recvWindow, newEvent);
                return true;
            }
        }
        return false;
    }
    else if (obj == _searchWindow) {
        if (event->type() == QEvent::KeyPress) {
            QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event);

            int key = keyEvent->key();
            int mods = keyEvent->modifiers();
            /* Enter/Return specifies going forward, or backward if shift is
               also held down.
               This mirrors how Shift+Tab (Backtab) is the reverse of Tab. */
            if (key == Qt::Key_Enter || key == Qt::Key_Return) {
                if (mods == 0) {
                    _searchBar->catchSearchNext();
                    return true;
                }
                else if (mods == Qt::ShiftModifier) {
                    _searchBar->catchSearchPrevious();
                    return true;
                }
            }
            else if (key == Qt::Key_Escape && mods == 0) {
                _searchBar->hide();
                return true;
            }
        }
    }
    else {
        if (event->type() == QEvent::Wheel) {
            QScrollBar *sb = _recvWindow->verticalScrollBar();
            QWheelEvent *wheelEvent = static_cast<QWheelEvent *>(event);
            if (sb->value() == sb->maximum() &&
                wheelEvent->angleDelta().y() <= 0) {
                /* If the user is at the bottom of the top window and scrolls
                   down, then swing focus to the send window. */
                _sendWindow->setFocus();
            }
        }
    }

    if (event->type() == QEvent::FocusIn && _current)
        emit focusChanged();

    return false;
}

void ESession::executeAnsi()
/* Evaluated stored ANSI codes, then prepare for more codes. */
{
    int code, i, prop;
    for (i = 0;i < _ansiCodeList.size();i++) {
        code = _ansiCodeList.at(i);
        switch (code) {
            case ANSI_RESET:
                if (_ansiAttributes != 0) {
                    if (_ansiAttributes & ANSI_ATTR_UNDERLINE)
                        _recvFormat.setFontUnderline(false);
                    if (_ansiAttributes & ANSI_ATTR_FLASHING)
                        _recvFormat.setFontOverline(false);
                    if (_ansiAttributes & ANSI_ATTR_ITALIC)
                        _recvFormat.setFontItalic(false);
                    if (_ansiAttributes & ANSI_ATTR_BRIGHT)
                        _ansiAdjust = 0;

                    _ansiAttributes = 0;
                }
                _recvFormat.setBackground(_profile->color(C_BACKGROUND));
                _recvFormat.setForeground(_profile->color(C_FOREGROUND));
                _recvFormat.setProperty(BG_PROP, C_BACKGROUND);
                _recvFormat.setProperty(FG_PROP, C_FOREGROUND);
                break;
            case ANSI_BLACK_FG:
            case ANSI_RED_FG:
            case ANSI_GREEN_FG:
            case ANSI_YELLOW_FG:
            case ANSI_BLUE_FG:
            case ANSI_MAGENTA_FG:
            case ANSI_CYAN_FG:
            case ANSI_WHITE_FG:
                prop = code - ANSI_FG_COLOR_OFFSET;
                if (_ansiAttributes & ANSI_ATTR_BRIGHT)
                    prop += _ansiAdjust;

                if (!(_ansiAttributes & ANSI_ATTR_SWAPPED)) {
                    _recvFormat.setForeground(_profile->color(prop));
                    _recvFormat.setProperty(FG_PROP, prop);
                }
                else {
                    _recvFormat.setBackground(_profile->color(prop));
                    _recvFormat.setProperty(BG_PROP, prop);
                }
                break;
            case ANSI_BLACK_BG:
            case ANSI_RED_BG:
            case ANSI_GREEN_BG:
            case ANSI_YELLOW_BG:
            case ANSI_BLUE_BG:
            case ANSI_MAGENTA_BG:
            case ANSI_CYAN_BG:
            case ANSI_WHITE_BG:
                prop = code - ANSI_BG_COLOR_OFFSET;

                if (!(_ansiAttributes & ANSI_ATTR_SWAPPED)) {
                    _recvFormat.setBackground(_profile->color(prop));
                    _recvFormat.setProperty(BG_PROP, prop);
                }
                else {
                    _recvFormat.setForeground(_profile->color(prop));
                    _recvFormat.setProperty(FG_PROP, prop);
                }
                break;
            case ANSI_NORMAL:
                _ansiAttributes &= ~ANSI_ATTR_BRIGHT;
                _ansiAdjust = 0;
                break;
            case ANSI_BRIGHT:
                _ansiAttributes |= ANSI_ATTR_BRIGHT;
                _ansiAdjust = 8;
                break;
            case ANSI_ITALICIZE:
                _recvFormat.setFontItalic(true);
                _ansiAttributes |= ANSI_ATTR_ITALIC;
                break;
            case ANSI_UNDERLINE:
                _recvFormat.setFontUnderline(true);
                _ansiAttributes |= ANSI_ATTR_UNDERLINE;
                break;
            case ANSI_REVERSE: /* Reverse background and foreground */
                {
                    int storeProp = _recvFormat.property(BG_PROP).toInt();
                    QColor storeColor = _recvFormat.background().color();
                    _recvFormat.setBackground(_recvFormat.foreground());
                    _recvFormat.setForeground(storeColor);

                    _recvFormat.setProperty(BG_PROP, _recvFormat.property(FG_PROP).toInt());
                    _recvFormat.setProperty(FG_PROP, storeProp);
                    _ansiAttributes |= ANSI_ATTR_SWAPPED;
                }
                break;
            case ANSI_HIDDEN:
                _recvFormat.setForeground(_recvFormat.background());
                _recvFormat.setProperty(FG_PROP, _recvFormat.property(BG_PROP).toInt());
                break;
            /* else it's a bad code, or just one that isn't supported here. */
        }
    }

    _ansiCodeList.clear();
    _ansiCurrentCode = 0;
}

void ESession::processAnsi(bool sendText)
/* Run through the text, parsing ECMA-48 codes along the way. Broken and
   non-color codes are ignored. */
{
    _textIndex = 0;
    _ansiBufferIndex = 0;
    char ch = 0;

    while (1) {
        switch (_ansiState) {
            case AnsiText:
                while (1) {
                    ch = _ansiBuffer[_ansiBufferIndex];
                    if (ch == '\0' || ch == ANSI_ESC)
                        break;

                    _textBuffer[_textIndex] = ch;
                    _textIndex++;
                    _ansiBufferIndex++;
                }

                if (_textIndex != 0 && sendText) {
                    _textBuffer[_textIndex] = '\0';
                    _recvCursor.insertText(_textBuffer, _recvFormat);
                    _textSent = true;
                }

                _textIndex = 0;
                if (ch == '\0')
                    break;
                else {
                    _ansiState = AnsiNeedBrace;
                    _ansiBufferIndex++;
                }

                /* Have ESC, so intentionally fall to [ capture. */
            case AnsiNeedBrace:
                ch = _ansiBuffer[_ansiBufferIndex];
                if (ch == '\0')
                    break;

                _ansiBufferIndex++;

                /* Give up on ill-formed ansi. */
                if (ch != '[') {
                    _ansiState = AnsiText;
                    continue;
                }

                _ansiState = AnsiValue;

                /* Intentionally fall from ESC[ to value processing. */
            case AnsiValue:
                while (1) {
                    ch = _ansiBuffer[_ansiBufferIndex];
                    if (ch >= '0' && ch <= '9') {
                        _ansiCurrentCode *= 10;
                        _ansiCurrentCode += ch - '0';
                    }
                    else if (ch == ';') {
                        _ansiCodeList.append(_ansiCurrentCode);
                        _ansiCurrentCode = 0;
                    }
                    else if (ch == '\0')
                        break;
                    /* These are probably letters that indicate operations. The
                       only supported one is m (color codes). The rest are
                       ignored. */
                    else {
                        _ansiCodeList.append(_ansiCurrentCode);
                        if (ch == 'm')
                            executeAnsi();

                        _ansiState = AnsiText;

                        /* Keep the operation letter from appearing in the text
                           by moving to the next thing. */
                        _ansiBufferIndex++;
                        /* Update ch so that \0 causes the loop to be left. */
                        ch = _ansiBuffer[_ansiBufferIndex];
                        break;
                    }

                    _ansiBufferIndex++;
                }
                if (ch != '\0')
                    continue;
                else
                    break;
        }

        if (ch == '\0')
            break;
    }
}

/* Process telnet codes that are within _inputBuffer. This will adjust
   _inputIndex to be after the telnet data. */
void ESession::processTelnet()
{
    char ch;

    while (1) {
        switch(_telnetState) {
            case TelnetText:
            case TelnetStart:
                /* IAC was just hit. Need DO/DONT/WILL/WONT for a command or SB
                   for subnegotiation. */
                _inputIndex++;
                ch = _inputBuffer[_inputIndex];

                if (ch == DO || ch == DONT || ch == WILL || ch == WONT) {
                    _inputIndex++;
                    _telnetState = TelnetOption;
                    continue;
                }
                else if (ch == SB) {
                    _inputIndex++;

                    _telnetState = TelnetSubnegStart;
                    continue;
                }
                else if (ch == '\0') {
                    _telnetState = TelnetStart;
                    break;
                }
                else if (ch == GA || ch == NOP) {
                    /* IAC GA means okay to send data (ignored)
                       IAC NOP does nothing.
                       Others will be ignored too. */
                    _inputIndex++;
                    _telnetState = TelnetText;
                }
                else {
                    /* This shouldn't happen, so log it to the client. It may
                       help to find a bug later. */
                    writeMessage(tr("Warning: Unexpected telnet operation %1.\n")
                            .arg((int)(unsigned char)ch, 3, 10, QChar('0')));
                    _inputIndex++;
                    _telnetState = TelnetText;
                    break;
                }
                break;
            case TelnetOption:
                ch = _inputBuffer[_inputIndex];
                if (ch == '\0')
                    break;
                else {
                    _inputIndex++;

                    if (_inputBuffer[_inputIndex] == IAC) {
                        _telnetState = TelnetStart;
                        continue;
                    }
                    else {
                        _telnetState = TelnetText;
                        break;
                    }
                }

                break;
            case TelnetSubnegStart:
                /* Have IAC SB, now need the option to ignore. */
                ch = _inputBuffer[_inputIndex];
                if (ch == '\0')
                    break;

                _inputIndex++;
                _telnetState = TelnetSubneg;

                /* Fall though to read the subnegotiation data. */
            case TelnetSubneg:
                /* Subnegotiation ends after receiving IAC SE. */
                while (1) {
                    ch = _inputBuffer[_inputIndex];
                    _inputIndex++;

                    if (ch == IAC)
                        _telnetEscapeOnSE = true;
                    else if (ch == SE) {
                        if (_telnetEscapeOnSE == true) {
                            _telnetEscapeOnSE = false;
                            ch = _inputBuffer[_inputIndex];
                            if (ch == IAC) {
                                _telnetState = TelnetStart;
                                break;
                            }
                            else {
                                _telnetState = TelnetText;
                                break;
                            }
                        }
                    }
                    else if (ch == '\0')
                        break;
                    else
                        _telnetEscapeOnSE = false;
                }
                if (_telnetState == TelnetStart)
                    continue;
        }
        break;
    }
}

void ESession::readyRead()
{
    /* Scroll to the bottom only if the cursor is on the bottom. */
    bool scrollToEnd = _sendWindow->hasFocus();

    _recvCursor.movePosition(QTextCursor::End);

    while (1) {
        int bytesRead = _socket->readLine(_inputBuffer, 4096);

        if (bytesRead <= 0)
            break;

        /* If true, _inputBuffer does not contain special codes. This allows
           triggers to test it without copying it over. */
        bool haveAnsi = false;

        _inputIndex = 0;
        _textIndex = 0;
        _ansiBufferIndex = 0;
        _inputBuffer[bytesRead] = '\0';

        /* If the last read was in telnet, then finish with that. */
        if (_telnetState != TelnetText) {
            processTelnet();
            if (_inputBuffer[_inputIndex] == '\0')
                continue;
        }
        /* Also check for ansi too... */
        if (_ansiState != AnsiText) {
            haveAnsi = true;
            char ch = _inputBuffer[_inputIndex];
            _ansiBuffer[_ansiBufferIndex] = ch;
            _ansiBufferIndex++;

            _inputIndex++;
            ch = _inputBuffer[_inputIndex];

            while (ch != 'm' && ch != '\0') {
                _ansiBuffer[_ansiBufferIndex] = ch;
                _ansiBufferIndex++;
                _inputIndex++;
                ch = _inputBuffer[_inputIndex];
            }

            if (ch == '\0') {
                _ansiBuffer[_ansiBufferIndex] = '\0';
                /* No text, just ansi. Update the ansi state, then go back for
                   more data. */
                processAnsi(true);
                continue;
            }
        }

        while (1) {
            char ch;
            if (_inputBuffer[_inputIndex] == IAC) {
                processTelnet();
                if (_inputBuffer[_inputIndex] == '\0')
                    break;
            }
            if (_inputBuffer[_inputIndex] == ANSI_ESC) {
                haveAnsi = true;
                _ansiBuffer[_ansiBufferIndex] = ANSI_ESC;
                _ansiBufferIndex++;

                _inputIndex++;
                ch = _inputBuffer[_inputIndex];

                while (ch != 'm' && ch != '\0') {
                    _ansiBuffer[_ansiBufferIndex] = ch;
                    _ansiBufferIndex++;
                    _inputIndex++;
                    ch = _inputBuffer[_inputIndex];
                }

                if (ch == 'm') {
                    _ansiBuffer[_ansiBufferIndex] = 'm';
                    _ansiBufferIndex++;
                    _inputIndex++;
                    ch = _inputBuffer[_inputIndex];
                }

                if (ch == '\0')
                    break;
                else
                    continue;
            }
            ch = _inputBuffer[_inputIndex];

            _ansiBuffer[_ansiBufferIndex] = ch;
            _ansiBufferIndex++;
            _textBuffer[_textIndex] = ch;
            _textIndex++;

            if (_inputBuffer[_inputIndex] == '\0')
                break;

            _inputIndex++;
        }

        _ansiBuffer[_ansiBufferIndex] = '\0';
        _textBuffer[_textIndex] = '\0';

        if (processTriggers() == true) {
            if (haveAnsi == true)
                processAnsi(true);
            else {
                _recvCursor.insertText(_textBuffer, _recvFormat);
                _textSent = true;
            }
        }

        /* Update the scrollbar if there's new text. */
        if (_textSent && scrollToEnd) {
            int scrollMax = _recvWindow->verticalScrollBar()->maximum();
            _recvWindow->verticalScrollBar()->setValue(scrollMax);
        }
    }
}

void ESession::resetAppearance(bool changes[32])
{
    /* This handles updating the defaults. */
    updateDisplay();

    /* The document is composed of different blocks, which represent the lines
       of the document. Those lines contain fragments, which represent an area
       of text with the same format. So each fragment in a block is guaranteed
       to have a different format from the previous ones.
       FG_PROP and BG_PROP are custom properties on the format of each fragment.
       and they hold what color was used. So loop over all blocks (and all
       fragments held) and adjust colors accordingly.
       This is called after the session editor is run and has changes. */
    QTextBlock block = _recvWindow->document()->begin();
    QTextCursor cursor = _recvCursor;

    while (block.isValid()) {
        QTextBlock::iterator it;
        for (it = block.begin(); !(it.atEnd()); ++it) {
            QTextFragment fragment = it.fragment();
            if (fragment.isValid()) {
                QTextCharFormat format = fragment.charFormat();

                int fgProp = format.property(FG_PROP).toInt();

                /* Make sure that this segment actually requires an update. */
                if (!changes[fgProp] &&
                    !changes[format.property(BG_PROP).toInt()])
                    continue;

                /* Only fix the foreground if it's not a user highlight. */
                if (fgProp != C_USER_HIGHLIGHT)
                    format.setForeground(_profile->color(fgProp));

                format.setBackground(_profile->color(format.property(BG_PROP).toInt()));

                cursor.setPosition(fragment.position());
                cursor.setPosition(fragment.position() + fragment.length(),
                                   QTextCursor::KeepAnchor);
                cursor.setCharFormat(format);
            }
        }
        block = block.next();
    }
}

void ESession::save(QString path)
{
    QFile file(path);
    QString title = _profile->title();

    if (!file.open(QFile::WriteOnly | QFile::Text)) {
        writeMessage(tr("Error: Failed to save log to '%1'.").arg(path));
        return;
    }

    QTextStream outStream(&file);
    outStream << _recvWindow->toPlainText().toUtf8();
    file.close();

    /* The save was successful so, remember the place for next time. */
    _logfilePath = path;
    writeMessage(tr("Saved log to '%1'.\n").arg(path));
}

void ESession::updateDisplay()
/* Update the colors of both the send and receive window. */
{
    _recvFormat.setForeground(_profile->color(_recvFormat.property(FG_PROP).toInt()));
    _recvFormat.setBackground(_profile->color(_recvFormat.property(BG_PROP).toInt()));

    QPalette PaletteA;
    PaletteA.setColor(QPalette::Base, _profile->color(C_BACKGROUND));
    _recvWindow->setPalette(PaletteA);

    QPalette PaletteB;
    PaletteB.setColor(QPalette::Base, _profile->color(C_BACKGROUND));
    PaletteB.setColor(QPalette::Text, _profile->color(C_FOREGROUND));
    _sendWindow->setPalette(PaletteB);
}

void ESession::writeMessage(QString message)
{
    _recvCursor.movePosition(QTextCursor::End);

    /* Make sure any current ansi format doesn't bleed in. */
    QBrush saveForeground = _recvFormat.foreground();
    QBrush saveBackground = _recvFormat.background();
    int saveFGProp = _recvFormat.property(FG_PROP).toInt();
    int saveBGProp = _recvFormat.property(BG_PROP).toInt();
    bool doUnderline = _recvFormat.fontUnderline();
    bool doOverline = _recvFormat.fontOverline();
    bool scrollToBottom;

    if (_recvWindow->verticalScrollBar()->value() ==
        _recvWindow->verticalScrollBar()->maximum())
        scrollToBottom = true;
    else
        scrollToBottom = false;

    /* Apply the message foreground and background. */
    _recvFormat.setForeground(_profile->color(C_MSG_FOREGROUND));
    _recvFormat.setBackground(_profile->color(C_MSG_BACKGROUND));
    _recvFormat.setProperty(FG_PROP, C_MSG_FOREGROUND);
    _recvFormat.setProperty(BG_PROP, C_MSG_BACKGROUND);
    _recvFormat.setFontOverline(false);
    _recvFormat.setFontUnderline(false);

    _recvCursor.insertText(message, _recvFormat);

    /* Revert back to how things were, in case this was in some ansi. */
    _recvFormat.setForeground(saveForeground);
    _recvFormat.setBackground(saveBackground);
    _recvFormat.setProperty(FG_PROP, saveFGProp);
    _recvFormat.setProperty(BG_PROP, saveBGProp);
    _recvFormat.setFontOverline(doOverline);
    _recvFormat.setFontUnderline(doUnderline);

    if (scrollToBottom) {
        int scrollMax = _recvWindow->verticalScrollBar()->maximum();
        _recvWindow->verticalScrollBar()->setValue(scrollMax);
    }
}

void ESession::start()
{
    bool dummy;
    int port = _profile->hostPort().toInt(&dummy, 10);

    connect(_socket, SIGNAL(connected()), this, SLOT(connected()));
    connect(_socket, SIGNAL(disconnected()), this, SLOT(disconnected()));
    connect(_socket, SIGNAL(error(QAbstractSocket::SocketError)), this,
            SLOT(error(QAbstractSocket::SocketError)));

    _socket->connectToHost(_profile->hostAddress(), port,
            QIODevice::ReadWrite);
}
